/*
 * Copyright the original author or authors.
 * 
 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.mozilla.org/MPL/MPL-1.1.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.blitscreen.camera
{
	import flash.display.DisplayObject;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	
	import com.blitscreen.events.FastEventBroadcaster;
	import com.bourre.structures.Range;	

	/**
	 * A <code>Camera</code> is a helper that represents a virtual screen
	 * as a <code>Rectangle</code> object, where position is an offset
	 * to subtract to on-screen objects position.
	 * <p>
	 * Instead of moving each objects to perform scrolling, you simply have
	 * to move the camera and all scrolling entities will adjust themselves
	 * according to the camera data.
	 * </p><p>
	 * <code>Camera</code> moves can be constrained by using a second 
	 * <code>Rectangle</code> object. In that case the camera screen 
	 * can go outside of this rectangle.
	 * </p><p>
	 * Optionnaly the camera can provide a zoom functionnality. Basically
	 * the zoom parameter is a float value that multiply the screen size.
	 * If <code>CameraListener</code> objects associated with that camera
	 * don't implement the zoom functionnality, it result that zoom is simply
	 * a screen resize. 
	 * </p><p>
	 * The <code>Camera</code> object dispath an <code>onCameraChange</code>
	 * event when one of the properties of the camera changed.
	 * </p><p>
	 * <strong>Warning :</strong> The zoom parameter has no effet on Bitmap
	 * based layer, as <code>BitmapData.copyPixel</code> doesn't support
	 * transformation matrix.
	 * </p>
	 * @author 	Cédric Néhémie
	 * @see		com.blitscreen.camera.CameraListener
	 * @see		com.blitscreen.camera.CameraEvent
	 */
	public class Camera
	{
		
		/**
		 * Composed EventBroadcaster
		 */
		protected var _oEB : FastEventBroadcaster;
		
		/**
		 * Current value of zoom applied to the camera
		 */
		protected var _nZoom : Number;
		
		/**
		 * Current value of zoom increment/decrement
		 */
		protected var _nZoomIncrement : Number;
		
		/**
		 * Range of values for the zoom
		 */
		protected var _oZoomRange : Range;
		
		/**
		 * Original width of the screen.
		 * <p>
		 * Used as orinigal value when calculate the final screen
		 * modified by zoom.
		 */
		protected var _nSafeWidth : Number;
		
		/**
		 * Original height of the screen.
		 * <p>
		 * Used as orinigal value when calculate the final screen
		 * modified by zoom.
		 * </p>
		 */
		protected var _nSafeHeight : Number;
		
		/**
		 * The current screen state
		 */
		protected var _rScreen : Rectangle;
		
		/**
		 * Rectangle where the camera can moves.
		 * <p>
		 * If null, no constraints are applied to the camera.
		 * </p>
		 */
		protected var _rConstraints : Rectangle;
		
		/**
		 * Creates a new camera instance that initaly display the passed-in
		 * <code>Rectangle</code>. That rectangle is considered to be 
		 * zoom-independant, it represent the size of the screen with a zoom
		 * of 1 (no zoom).
		 * <p>
		 * Optionnaly the constructor can accept an initial zoom value, 
		 * a zoom range and and a <code>Rectangle</code> object, which
		 * represent the camera constraints.
		 * </p>
		 * @param screen		<code>Rectangle</code> screen region that
		 * 						is initialy displayed by the camera
		 * @param constraints	<code>Rectangle</code> to constraints camera moves, 
		 * 						if not defined the camera have no constraints
		 * @param initialZoom	<code>Number</code> value for the camera zoom.
		 * 						If not defined the value is set to 1.
		 * 						The zoom is automatically applied on the
		 * 						screen at creation
		 * @param zoomRange		<code>Range</code> of values for the zoom, zoom
		 * 						values can't go outside of that range. If the
		 * 						range isn't define, the range goes from
		 * 						<code>0</code> to <code>POSITIVE_INFINITY</code>
		 * @param zoomIncrement	value used for zoom/unzoom process
		 */
		public function Camera ( screen : Rectangle,
								 constraints : Rectangle = null,
								 initialZoom : Number = 1,
								 zoomRange : Range = null, 
								 zoomIncrement : Number = 0 )
		{
			_rScreen = screen;
			_nSafeWidth = screen.width;
			_nSafeHeight = screen.height;
			_nZoom = initialZoom;
			_nZoomIncrement = zoomIncrement;
			_oZoomRange = zoomRange ? zoomRange : new Range( 0 );
			_rConstraints = constraints;
			
			_oEB = new FastEventBroadcaster ( this, CameraListener );
			
			setZoom( _nZoom );
		}
		
		/**
		 * Provides an access to the top left corner x coordinate
		 * of the screen. That property allow to read and write values.
		 */
		public function get x () : Number
		{
			return _rScreen.x;	
		}
		/**
		 * @private
		 */
		public function set x ( n : Number ) : void
		{
			moveToX( n );
		}
		
		/**
		 * Provides an access to the top left corner y coordinate
		 * of the screen. That property allow to read and write values.
		 */
		public function get y () : Number
		{
			return _rScreen.y;	
		}
		/**
		 * @private
		 */
		public function set y ( n : Number ) : void
		{
			moveToY( n );
		}
		
		/**
		 * Provides an access to the center x coordinate of the screen. 
		 * That property allow to read and write values.
		 */
		public function get focusX () : Number
		{
			return getScreenCenter().x;	
		}
		/**
		 * @private
		 */
		public function set focusX ( n : Number ) : void
		{
			centerX( n );
		}
		
		/**
		 * Provides an access to the center y coordinate of the screen. 
		 * That property allow to read and write values.
		 */
		public function get focusY () : Number
		{
			return getScreenCenter().y;	
		}
		/**
		 * @private
		 */
		public function set focusY ( n : Number ) : void
		{
			centerY( n );
		}
		
		/**
		 * Provides an access to the width of the screen. 
		 * That property allow to read and write values.
		 */
		public function get width () : Number
		{
			return _rScreen.width;	
		}
		/**
		 * @private
		 */
		public function set width ( n : Number ) : void
		{
			resizeToW( n );
		}
		
		/**
		 * Provides an access to the height of the screen. 
		 * That property allow to read and write values.
		 */
		public function get height () : Number
		{
			return _rScreen.width;	
		}
		/**
		 * @private
		 */
		public function set height ( n : Number ) : void
		{
			resizeToH( n );
		}

		/**
		 * Returns a <code>Rectangle</code> instance that represent the current
		 * screen state.
		 * 
		 * @return <code>Rectangle</code> that represent the current screen state
		 */
		public function getScreen () : Rectangle
		{
			return _rScreen;
		}
				
		/**
		 * Defines the constraints for the camera moves.
		 * <p>
		 * If  constraints are null the camera can move
		 * without any restrictions.
		 * </p>
		 * @param constraints <code>Rectangle</code> of constraints
		 * 		  for the camera
		 */
		public function setConstraints ( constraints : Rectangle = null ) : void
		{
			_rConstraints = constraints;
		}
		
		/**
		 * Returns a <code>Rectangle</code> instance representing
		 * the camera constraints.
		 * 
		 * @return <code>Rectangle</code> instance representing
		 * the camera constraints.
		 */
		public function getConstraints () : Rectangle
		{
			return _rConstraints;
		}
		
		/**
		 * Returns the center of the current screen as a <code>Point</code> object.
		 * 
		 * @return <code>Point</code> center of the current screen
		 */
		public function getScreenCenter () : Point
		{
			return new Point (	_rScreen.x + _rScreen.width / 2, _rScreen.y + _rScreen.height / 2 );
		}
		
		/**
		 * Centers the screen with the passed-in <code>Point</code>
		 * as coordinates.
		 * 
		 * @param p <code>Point</code> coordinates to center the screen
		 */
		public function center ( p : Point ) : void
		{
			centerXY ( p.x, p.y );
		}
		
		/**
		 * Centers the screen with the passed-in coordinates.
		 * 
		 * @param x	<code>Number</code> value for X
		 * @param y <code>Number</code> value for Y
		 */
		public function centerXY ( x : Number, y : Number ) : void
		{
			var nx : Number = x - _rScreen.width / 2 ;			var ny : Number = y - _rScreen.height / 2;
			
			if( nx !=  _rScreen.x || ny != _rScreen.y )
			{
				_rScreen.x = nx;
				_rScreen.y = ny;
							
				solve ();
				
				fireOnCameraChangeEvent();
			}
		}
		
		/**
		 * Centers the screen on the X axis with the passed-in coordinate.
		 * 
		 * @param x	<code>Number</code> value for X axis
		 */
		public function centerX ( x : Number ) : void
		{
			var nx : Number = x - _rScreen.width / 2 ;
			
			if( nx != _rScreen.x )
			{
				_rScreen.x = nx ;
				
				solve ();
				
				fireOnCameraChangeEvent();
			}
		}
		 
		/**
		 * Centers the screen on the Y axis with the passed-in coordinate.
		 * 
		 * @param y	<code>Number</code> value for Y axis
		 */
		public function centerY ( y : Number ) : void
		{
			var ny : Number = y - _rScreen.height / 2 ;
			
			if( ny != _rScreen.y )
			{
				_rScreen.y = ny ;
				
				solve ();
				
				fireOnCameraChangeEvent();
			}
		} 
		
		/**
		 * 
		 * @param display
		 * 
		 */
		public function centerDisplayObject ( display : DisplayObject ) : void
		{
			var r : Rectangle = display.getBounds( display.parent );
			var x : Number = r.x + r.width / 2;
			var y : Number = r.y + r.height / 2;
			
			centerXY( x, y );
		}
		
		/**
		 * Moves the top-left corner of the screen to the passed-in
		 * <code>Point</code> coordinates.
		 * 
		 * @param p <code>Point</code> coordinates for the top-left
		 * 			corner of the screen.
		 */
		public function moveTo ( p : Point ) : void
		{
			moveToXY( p.x, p.y );
		}		
		/**
		 * Moves the top-left corner of the screen to the passed-in coordinates.
		 * 
		 * @param x	<code>Number</code> value for X
		 * @param y <code>Number</code> value for Y
		 */
		public function moveToXY ( x : Number, y : Number ) : void
		{
			_rScreen.x = x;
			_rScreen.y = y;
			
			solve ();
			
			fireOnCameraChangeEvent();
		}		
		/**
		 * Moves the top-left corner x coordinate of the screen
		 * to the passed-in coordinate.
		 * 
		 * @param x	<code>Number</code> value for X
		 */
		public function moveToX ( x : Number ) : void
		{
			_rScreen.x = x;
			
			solve ();
			
			fireOnCameraChangeEvent();
		}
		/**
		 * Moves the top-left corner y coordinate of the screen
		 * to the passed-in coordinate.
		 * 
		 * @param y	<code>Number</code> value for Y
		 */
		public function moveToY ( y : Number ) : void
		{
			_rScreen.y = y;
			
			solve ();
			
			fireOnCameraChangeEvent();
		}
		
		/**
		 * Adds the passed-in <code>Point</code> coordinates to the current
		 * top-left corner location.
		 * 
		 * @param p <code>Point</code> coordinates to add to the top-left
		 * 			corner of the screen.
		 */
		public function translate ( p : Point ) : void
		{
			translateXY ( p.x, p.y );
		}			
		/**
		 * Adds the passed-in values to the current
		 * top-left corner location.
		 * 
		 * @param x	<code>Number</code> value for X
		 * @param y <code>Number</code> value for Y
		 */
		public function translateXY ( x : Number, y : Number ) : void
		{
			_rScreen.offset( x, y );
			
			solve ();
			
			fireOnCameraChangeEvent();
		}		
		/**
		 * Adds the passed-in value to the current
		 * top-left corner x location.
		 * 
		 * @param x	<code>Number</code> value for X
		 */
		public function translateX ( x : Number ) : void
		{
			_rScreen.offset( x, 0 );
			
			solve ();
			
			fireOnCameraChangeEvent();
		}
		/**
		 * Adds the passed-in value to the current
		 * top-left corner y location.
		 * 
		 * @param y	<code>Number</code> value for Y
		 */
		public function translateY ( y : Number ) : void
		{
			_rScreen.offset( 0, y );
			
			solve ();
			
			fireOnCameraChangeEvent();
		}
		
		/**
		 * Resizes the camera screen to the passed-in <code>Point</code>
		 * values.
		 * <p>
		 * Resizing the camera doesn't modify the value of the zoom, 
		 * if you want to perform a zoom, use the <code>setZoom<code>
		 * method instead. 
		 * </p>
		 * @param p <code>Point</code> new size of the screen
		 */
		public function resizeTo ( p : Point ) : void
		{
			resizeToWH ( p.x, p.y );
		}
		
		/**
		 * Resizes the camera screen to the passed-in values.
		 * <p>
		 * Resizing the camera doesn't modify the value of the zoom, 
		 * if you want to perform a zoom, use the <code>setZoom<code> 
		 * method instead. 
		 * </p>
		 * @param w	<code>Number</code> value for the width
		 * @param h <code>Number</code> value for the height
		 */
		public function resizeToWH ( w : Number, h : Number ) : void
		{
			_nSafeWidth = w;
			_nSafeHeight = h;
			
			solveZoom();
		}
		
		/**
		 * Resizes the camera screen width to the passed-in value.
		 * <p>
		 * Resizing the camera doesn't modify the value of the zoom, 
		 * if you want to perform a zoom, use the <code>setZoom<code> 
		 * method instead. 
		 * </p>
		 * @param w	<code>Number</code> value for the width
		 */
		public function resizeToW ( w : Number ) : void
		{
			_nSafeWidth = w;
			
			solveZoom();
		}
		
		/**
		 * Resizes the camera screen height to the passed-in value.
		 * <p>
		 * Resizing the camera doesn't modify the value of the zoom, 
		 * if you want to perform a zoom, use the <code>setZoom<code> 
		 * method instead. 
		 * </p>
		 * @param h	<code>Number</code> value for the height
		 */
		public function resizeToH ( h : Number ) : void
		{
			_nSafeHeight = h;
			
			solveZoom();
		}
		
		/**
		 * Defines the zoom values for the camera.
		 * <p>
		 * The size and coordinates of the screen is modified
		 * after the call. 
		 * </p><p>
		 * Zoomed screen is calculated as : 
		 * <pre>newWidth = width * zoom;
		 * newHeight = height * zoom;</pre>
		 * </p>
		 * @param zoom 	<code>Number</code> value of the zoom.     
		 */
		public function setZoom ( zoom : Number ) : void
		{
			if( _oZoomRange.surround( zoom ) )
			{
				_nZoom = zoom;
				computeZoom();
			}
		}	
		
		/**
		 * Returns the current zoom value for the camera.
		 * 
		 * @return <code>Number</code> current zoom value for the camera
		 */
		public function getZoom () : Number
		{
			return _nZoom;
		}
		
		/**
		 * Increment the zoom of this camera with the zoom increment value.
		 */
		public function zoom() : void
		{
			setZoom( _nZoom + _nZoomIncrement );
		}
		
		/**
		 * Decrement the zoom of this camera with the zoom increment value.
		 */
		public function unzoom() : void
		{
			setZoom( _nZoom - _nZoomIncrement );
		}

		/**
		 * Defines the new <code>Range</code> of values within which
		 * the zoom value have to be.
		 * 
		 * @param zoomRange <code>Range</code> of possibles values 
		 * 					for zoom
		 */
		public function setZoomRange ( zoomRange : Range ) : void
		{
			_oZoomRange = zoomRange;
			solveZoom();	
		}
		
		/**
		 * Returns the current <code>Range</code> of possibles values
		 * for the zoom.
		 * 
		 * @return <code>Range</code> of possibles values for the zoom.
		 */
		public function getZoomRange () : Range
		{
			return _oZoomRange;
		}
		
		/**
		 * Solve the bounds for the zoom and then apply it.
		 */
		protected function solveZoom () : void
		{
			var b : Boolean = false;
			
			if( _oZoomRange.min > _nZoom )
			{
				_nZoom = _oZoomRange.min;
				b = true;
			}
			
			if( _oZoomRange.max < _nZoom )
			{
				_nZoom = _oZoomRange.max;
				b = true;
			}
			
			if( b ) computeZoom();
		}
		
		/**
		 * Apply the new zoom to the screen;
		 */
		protected function computeZoom () : void
		{
			var p : Point = getScreenCenter();
			
			if( _nZoom == 0)
				return;
			
			var width : Number = _nSafeWidth / _nZoom;
			var height : Number = _nSafeHeight / _nZoom;
			
			_rScreen.width = width;
			_rScreen.height = height;
			
			center( p );
			//fireOnCameraChangeEvent();
		}
		
		/**
		 * Solve the camera constraints.
		 */
		protected function solve () :void
		{
			if( _rConstraints == null ) return;
			
			if( _rScreen.x < _rConstraints.x )
				_rScreen.x = _rConstraints.x;
			else if( _rScreen.right > _rConstraints.right )
				_rScreen.x = _rConstraints.right - _rScreen.width;
				
			if( _rScreen.y < _rConstraints.y )
				_rScreen.y = _rConstraints.y;
			else if( _rScreen.bottom > _rConstraints.bottom )
				_rScreen.y = _rConstraints.bottom - _rScreen.height;
		}
				
		/**
		 * Adds the passed-in <code>CameraListener</code> to listen
		 * the current camera.
		 * 
		 * @param listener <code>CameraListener</code> to add as listener.
		 * @return 	<code>true</code> if the listener have been succesfully
		 * 			added as listener, either <code>false</code>
		 */
		public function addCameraListener ( listener : CameraListener ) : Boolean
		{
			return _oEB.addListener( listener );
		}		
		/**
		 * Removes the passed-in <code>CameraListener</code> from listeners 
		 * of the current camera.
		 * 
		 * @param listener <code>CameraListener</code> to remove from listeners.
		 * @return 	<code>true</code> if the listener have been succesfully removed
		 * 		   	from listeners, either <code>false</code>
		 */
		public function removeCameraListener ( listener : CameraListener ) : Boolean
		{
			return _oEB.removeListener( listener );
		}		
		/**
		 * Dispatches changes from this camera to its listeners.
		 */
		protected function fireOnCameraChangeEvent () : void
		{
			var event : CameraEvent = new CameraEvent ( CameraEvent.onCameraChangeEVENT, this );
			_oEB.broadcastEvent( event );
		}
	}
}